require 'spec_helper'

describe SorceryController, type: :controller do
  describe 'plugin configuration' do
    before(:all) do
      sorcery_reload!
    end

    after do
      Sorcery::Controller::Config.reset!
      sorcery_reload!
    end

    it "enables configuration option 'user_class'" do
      sorcery_controller_property_set(:user_class, 'TestUser')

      expect(Sorcery::Controller::Config.user_class).to eq 'TestUser'
    end

    it "enables configuration option 'not_authenticated_action'" do
      sorcery_controller_property_set(:not_authenticated_action, :my_action)

      expect(Sorcery::Controller::Config.not_authenticated_action).to eq :my_action
    end

    it "enables configuration option 'use_redirect_back_or_to_by_rails'" do
      sorcery_controller_property_set(:use_redirect_back_or_to_by_rails, true)

      expect(Sorcery::Controller::Config.use_redirect_back_or_to_by_rails).to be true
    end
  end

  # ----------------- PLUGIN ACTIVATED -----------------------
  context 'when activated with sorcery' do
    let!(:user) { User.create!(username: 'test_user', email: 'test@example.com', password: 'password') }

    before(:all) do
      sorcery_reload!
    end

    after do
      Sorcery::Controller::Config.reset!
      sorcery_reload!
      sorcery_controller_property_set(:user_class, User)
      sorcery_model_property_set(:username_attribute_names, [:email])
    end

    it { is_expected.to respond_to(:login) }

    it { is_expected.to respond_to(:logout) }

    it { is_expected.to respond_to(:logged_in?) }

    it { is_expected.to respond_to(:current_user) }

    it { is_expected.to respond_to(:require_login) }

    describe '#login' do
      context 'when succeeds' do
        before do
          expect(User).to receive(:authenticate).with('bla@example.com', 'secret').and_yield(user, nil)
          get :test_login, params: { email: 'bla@example.com', password: 'secret' }
        end

        it 'assigns user to @user variable' do
          expect(assigns[:user]).to eq user
        end

        it 'writes user id in session' do
          expect(session[:user_id]).to eq user.id.to_s
        end
      end

      context 'when fails' do
        before do
          expect(User).to receive(:authenticate).with('bla@example.com', 'opensesame!').and_return(nil)
          get :test_login, params: { email: 'bla@example.com', password: 'opensesame!' }
        end

        it 'sets @user variable to nil' do
          expect(assigns[:user]).to be_nil
        end

        it 'sets user_id in session to nil' do
          expect(session[:user_id]).to be_nil
        end
      end
    end

    describe '#login!' do
      context 'when succeeds' do
        before do
          expect(User).to receive(:authenticate).with('bla@example.com', 'secret').and_yield(user, nil)
          get :test_login_bang, params: { email: 'bla@example.com', password: 'secret' }
        end

        it 'assigns user to @user variable' do
          expect(assigns[:user]).to eq user
        end

        it 'writes user id in session' do
          expect(session[:user_id]).to eq user.id.to_s
        end
      end

      context 'when fails' do
        before do
          expect(User).to receive(:authenticate).with('bla@example.com', 'opensesame!').and_return(nil)
        end

        it 'raises InvalidCredentials exception' do
          expect do
            get :test_login_bang, params: { email: 'bla@example.com', password: 'opensesame!' }
          end.to raise_error(Sorcery::InvalidCredentials)
        end
      end
    end

    describe '#login! with block' do
      context 'when succeeds' do
        before do
          expect(User).to receive(:authenticate).with('bla@example.com', 'secret').and_yield(user, nil)
          get :test_login_bang_with_block, params: { email: 'bla@example.com', password: 'secret' }
        end

        it 'writes user id in session' do
          expect(session[:user_id]).to eq user.id.to_s
        end

        it 'redirects to root' do
          expect(response).to redirect_to(root_url)
        end
      end

      context 'when fails' do
        before do
          expect(User).to receive(:authenticate).with('bla@example.com', 'opensesame!').and_return(nil)
        end

        it 'raises InvalidCredentials exception' do
          expect do
            get :test_login_bang_with_block, params: { email: 'bla@example.com', password: 'opensesame!' }
          end.to raise_error(Sorcery::InvalidCredentials)
        end
      end
    end

    describe '#logout' do
      it 'clears the session' do
        cookies[:remember_me_token] = nil
        session[:user_id] = user.id.to_s
        get :test_logout

        expect(session[:user_id]).to be_nil
      end
    end

    describe '#logged_in?' do
      it 'returns true when user is logged in' do
        session[:user_id] = user.id.to_s

        expect(subject.logged_in?).to be true
      end

      it 'returns false when user is not logged in' do
        session[:user_id] = nil

        expect(subject.logged_in?).to be false
      end
    end

    describe '#current_user' do
      it 'current_user returns the user instance if logged in' do
        session[:user_id] = user.id.to_s

        2.times { expect(subject.current_user).to eq user } # memoized!
      end

      it 'current_user returns false if not logged in' do
        session[:user_id] = nil

        2.times { expect(subject.current_user).to be_nil } # memoized!
      end
    end

    it "calls the configured 'not_authenticated_action' when authenticate before_action fails" do
      session[:user_id] = nil
      sorcery_controller_property_set(:not_authenticated_action, :test_not_authenticated_action)
      get :test_logout

      expect(response).to be_successful
    end

    it 'require_login before_action saves the url that the user originally wanted' do
      get :some_action

      expect(session[:return_to_url]).to eq 'http://test.host/some_action'
      expect(response).to redirect_to('http://test.host/')
    end

    it 'require_login before_action does not save the url that the user originally wanted upon all non-get http methods' do
      %i[post put delete].each do |m|
        send(m, :some_action)

        expect(session[:return_to_url]).to be_nil
      end
    end

    it 'require_login before_action does not save the url for JSON requests' do
      get :some_action, format: :json
      expect(session[:return_to_url]).to be_nil
    end

    it 'require_login before_action does not save the url for XHR requests' do
      get :some_action, xhr: true
      expect(session[:return_to_url]).to be_nil
    end

    it 'on successful login the user is redirected to the url he originally wanted' do
      session[:return_to_url] = 'http://test.host/some_action'
      post :test_return_to, params: { email: 'bla@example.com', password: 'secret' }

      expect(response).to redirect_to('http://test.host/some_action')
      expect(flash[:notice]).to eq 'haha!'
    end

    # --- auto_login(user) ---
    it { is_expected.to respond_to(:auto_login) }

    it 'auto_login(user) logs in a user instance' do
      session[:user_id] = nil
      subject.auto_login(user)

      expect(subject.logged_in?).to be true
    end

    it 'auto_login(user) works even if current_user was already set to false' do
      get :test_logout

      expect(session[:user_id]).to be_nil
      expect(subject.current_user).to be_nil

      expect(User).to receive(:first) { user }

      get :test_auto_login

      expect(assigns[:result]).to eq user
    end

    describe 'redirect_back_or_to' do
      describe 'use_redirect_back_or_to_by_rails' do
        context 'when true' do
          before do
            sorcery_controller_property_set(:use_redirect_back_or_to_by_rails, true)
            allow_any_instance_of(ActionController::TestRequest) # rubocop:disable RSpec/AnyInstance
              .to receive(:referer).and_return('http://test.host/referer_action')
          end

          it 'uses Rails 7 redirect_back_or_to method' do
            get :test_redirect_back_or_to

            expect(response).to redirect_to('http://test.host/referer_action')
          end
        end

        context 'when false' do
          before { sorcery_controller_property_set(:use_redirect_back_or_to_by_rails, false) }

          it 'uses Sorcery redirect_back_or_to method and warns about overriding the Rails 7 method' do
            deprecator = Sorcery.deprecator
            expected_message = '`redirect_back_or_to` overrides the method of the same name defined in Rails 7. ' \
                               'To avoid overriding, set `config.use_redirect_back_or_to_by_rails = true` and use `redirect_to_before_login_path`. ' \
                               'In a future release, `config.use_redirect_back_or_to_by_rails = true` will become the default.'

            expect(deprecator).to receive(:warn).with(expected_message)

            session[:return_to_url] = 'http://test.host/some_action'
            get :test_redirect_back_or_to

            expect(response).to redirect_to('http://test.host/some_action')
            expect(flash[:notice]).to eq 'haha!'
          end
        end
      end
    end

    describe '#redirect_to_before_login_path' do
      context 'when allow_other_host is true' do
        it 'redirects to external host' do
          get :test_redirect_to_before_login_path_with_allow_other_host

          expect(response).to redirect_to('http://external.example.com/')
          expect(flash[:notice]).to eq 'redirected!'
        end

        it 'redirects to session[:return_to_url] if present' do
          session[:return_to_url] = 'http://other.example.com/saved_path'
          get :test_redirect_to_before_login_path_with_allow_other_host

          expect(response).to redirect_to('http://other.example.com/saved_path')
          expect(session[:return_to_url]).to be_nil
        end
      end

      context 'when allow_other_host is false' do
        it 'raises an error when redirecting to external host' do
          expect do
            get :test_redirect_to_before_login_path_without_allow_other_host
          end.to raise_error(ActionController::Redirecting::UnsafeRedirectError, /Unsafe redirect/)
        end
      end
    end
  end
end
